/*
 * @(#)MimirContext.java	1.0 Apr 17, 2008
 *
 *	The MIT License
 *
 *	Copyright (c) 2008 Malachi de AElfweald <malachid@gmail.com>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 */
package org.eoti.mimir;

import org.eoti.spec.mimirdb.v1.DBMapping;
import org.eoti.xml.NamespacePrefixMapper;
import org.eoti.xml.NamespacePrefixMapperFactory;

import javax.xml.bind.*;
import static javax.xml.bind.Marshaller.JAXB_ENCODING;
import static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT;
import javax.xml.namespace.QName;
import java.io.*;
import java.net.URL;

/**
 * This class represents the underlying context responsible for reading/writing the database entries
 */
public class MimirContext<DATA,ID>
{
	protected MimirRegistry registry;
	protected JAXBContext jaxb;
	protected String indentString = "  ";//2 spaces?

	public MimirContext(MimirRegistry registry)
	{
		this.registry = registry;
	}

	public void clear()
	{
		jaxb = null;
	}

	protected JAXBContext getContext()
			throws MimirException
	{
		if(jaxb != null) return jaxb;

		StringBuilder sb = new StringBuilder();
		boolean firstMapping = true;
		for(DBMapping mapping : registry.getPrefixMapper().getMappings())
		{
			String specPackage = mapping.getSpecPackage();
			if(specPackage == null) continue;
			if(!firstMapping) sb.append(":");
			sb.append(specPackage);
			firstMapping = false;
		}

		try{
			jaxb = JAXBContext.newInstance(sb.toString(), this.getClass().getClassLoader());
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}

		return jaxb;
	}

	public Unmarshaller createUnmarshaller()
	throws MimirException
	{
		try{
			return getContext().createUnmarshaller();
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}
	}

	public Marshaller createMarshaller()
			throws MimirException
	{
		JAXBContext ctx = getContext();
		try{
			Marshaller marshaller = ctx.createMarshaller();
			try{
				NamespacePrefixMapper npm = NamespacePrefixMapperFactory.getInstance(registry.getPrefixMapper());

				try{
					marshaller.setProperty(npm.getMapperPropertyName(), npm);
				}catch(JAXBException ignorable){
					/**
					 * If JAXB doesn't recognize/accept the property, it will throw an exception.
					 * However, since being unable to provide a human friendly output is not a fatal error,
					 * we will simply ignore it and continue
					 */
				}

				try{
					marshaller.setProperty(npm.getIndentPropertyName(), indentString);
				}catch(JAXBException ignorable){
					/**
					 * If JAXB doesn't recognize/accept the property, it will throw an exception.
					 * However, since being unable to provide a human friendly output is not a fatal error,
					 * we will simply ignore it and continue
					 */
				}
			}catch(Exception e){
				/**
				 * Since being unable to provide a human friendly output is not a fatal error,
				 * we will simply ignore it and continue
				 */
			}
			

			try{
				marshaller.setProperty(JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
			}catch(JAXBException ignorable){
				/**
				 * If JAXB doesn't recognize/accept the property, it will throw an exception.
				 * However, since being unable to provide a human friendly output is not a fatal error,
				 * we will simply ignore it and continue
				 */
			}


			/**
			 * You could avoid adding the XMLDECL by doing:
			 * marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
			 *
			 * Can specify a xsi:schemaLocation using
			 * marshaller.setProperty("jaxb.schemaLocation", string)
			 *
			 * Can specify a xsi:noNamespaceSchemaLocation using
			 * marshaller.setProperty("jaxb.noNamespaceSchemaLocation", string)
			 */

			/**
			 * The wrong encoding, however, would be a fatal error
			 */
			marshaller.setProperty(JAXB_ENCODING, "UTF-8");

			return marshaller;
		} catch(Exception e){
			throw new MimirException(registry, e);
		}
	}

	public DATA read(File file)
	throws MimirException
	{
		if(file.lastModified() == 0) // ie: doesn't exist
			return null;

		try{
			return (DATA) ((JAXBElement)createUnmarshaller().unmarshal(file)).getValue();
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}
	}

	public DATA read(URL url)
	throws MimirException
	{
		try{
			return (DATA) ((JAXBElement)createUnmarshaller().unmarshal(url)).getValue();
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}
	}

	public DATA read(InputStream stream)
	throws MimirException
	{
		try{
			return (DATA) ((JAXBElement)createUnmarshaller().unmarshal(stream)).getValue();
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}
	}

	public DATA read(String fullyQualifiedResourceName)
	throws MimirException
	{
		URL url = this.getClass().getClassLoader().getResource(fullyQualifiedResourceName);
		if(url == null)
			throw new MimirException(registry, String.format("Resource not found: %s", fullyQualifiedResourceName));

		return read(url);
	}

	public void write(File file, DATA data, DBMapping mapping) // can I find the mapping rather than pass it?
	throws MimirException
	{
		file.getParentFile().mkdirs();
		try{
			write(new FileOutputStream(file), data, mapping);
			/**
			 * FileWriter doesn't do the encoding correctly
			 * https://jaxb.dev.java.net/servlets/ReadMsg?list=users&msgNo=4592
			 */
//			createMarshaller().marshal(element, new FileWriter(file));
		} catch(IOException e){
			throw new MimirException(registry, e);
		}
	}

	public void write(OutputStream out, DATA data, DBMapping mapping) // can I find the mapping rather than pass it?
	throws MimirException
	{
		try{
			QName qname = new QName(mapping.getNamespaceURI(), mapping.getNamespacePrefix());
			JAXBElement<DATA> element = new JAXBElement<DATA>(qname, (Class<DATA>)data.getClass(), null, data);
			createMarshaller().marshal(element, out);
		} catch(JAXBException e){
			throw new MimirException(registry, e);
		}
	}

	public void display(File file, DBMapping mapping)
			throws MimirException
	{
		display(file, System.out, mapping);
	}

	public void display(File file, PrintStream out, DBMapping mapping)
			throws MimirException
	{
		write(out, read(file), mapping);
	}

}
